package xj.graph2d.shapes.uml;

import static xj.graph2d.DrawShapeConstants.cpady;
import static xj.graph2d.DrawShapeConstants.PointEastEdge;
import static xj.graph2d.DrawShapeUtil.getShapeBounds;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.ArrayList;

import xj.graph2d.DrawAttr;
import xj.graph2d.DrawShape;
import xj.graph2d.DrawShapeBase;
import xj.graph2d.RectangleShape;
import xj.graph2d.GraphNodeBase;
import xj.graph2d.DrawShapeConstants;
import xj.graph2d.Label;
import xj.graph2d.shapes.CompartmentalRoundRectangleShape;
import xj.graph2d.shapes.Compartments;
import xj.graph2d.shapes.HorizontalCompartmentsShape;
import xj.meta.ModelElement;
import xj.model.uml.State;

import static xj.graph2d.DrawShapeUtil.moveShapes;

public class OrthogonalStateShape 
  extends CompoundStateShape 
  implements StateShape {

  public OrthogonalStateShape() {
  }

  public OrthogonalStateShape(float x, float y, float w, float h) {
    this(x, y, w, h, false, false);
  }

  public OrthogonalStateShape(float x, float y, float w, float h,
			      boolean createComp) {
    this(x, y, w, h, false, createComp);
  }


  /* 
   * By default the orthogonal state has two regions and 
   * an optional compartment for action 
   */
  public OrthogonalStateShape(float x, float y, float w, float h,
			      boolean showAction, boolean createComp) {
    super(new CompartmentalRoundRectangleShape(x, y, w, h - TITLE_HEIGHT,
					       showAction ? new float[] { 0.2f, 0.4f } : new float[] { 0.5f }),
	  new RectangleShape(x, y, w * 0.3f, TITLE_HEIGHT), x, y, w, h, createComp);
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    body.setNumberOfHorizontalCompartmentsFixed(false);
    body.setScaleTopCompartment(false);
    Compartments.DividerLineMode divMode;
    if (showAction) {
      divMode = Compartments.DividerLineMode.EveryLineExceptFirst;
    } else {
      divMode = Compartments.DividerLineMode.EveryLine;
    }
    body.setDividerStyle(divMode, DrawAttr.DASHED_LINE);
    actionCompartmentShown = showAction;
  }

  public OrthogonalStateShape(HorizontalCompartmentsShape mainShape,
			      RectangleShape titleShape, 
			      float x, float y, float w, float h) {
    this(mainShape, titleShape, false, x, y, w, h, false);
  }

  public OrthogonalStateShape(HorizontalCompartmentsShape mainShape,
			      RectangleShape titleShape, boolean showAction, 
			      float x, float y, float w, float h) {
    this(mainShape, titleShape, showAction, x, y, w, h, false);
  }

  public OrthogonalStateShape(HorizontalCompartmentsShape mainShape,
			      RectangleShape titleShape, boolean showAction, 
			      float x, float y, float w, float h, 
			      boolean createComp) {
    super(mainShape, titleShape, x, y, w, h, createComp);
    if (mainShape != null) {
      mainShape.setNumberOfHorizontalCompartmentsFixed(false);
      mainShape.setScaleTopCompartment(false);
      Compartments.DividerLineMode divMode;
      if (showAction) {
	divMode = Compartments.DividerLineMode.EveryLineExceptFirst;
      } else {
	divMode = Compartments.DividerLineMode.EveryLine;
      }
      mainShape.setDividerStyle(divMode, DrawAttr.DASHED_LINE);
      actionCompartmentShown = showAction;
    }
  }

  @Override
  public DrawShape makeInstance() 
    throws CloneNotSupportedException {

    float x1 = (float) p1.getX();
    float y1 = (float) p1.getY();
    float x2 = (float) p2.getX();
    float y2 = (float) p2.getY();
    float x = Math.min(x1, x2);
    float y = Math.min(y1, y2);
    float w = Math.abs(x2 - x1);
    float h = Math.abs(y2 - y1);
    return new OrthogonalStateShape((HorizontalCompartmentsShape) ((mainShape != null) ? mainShape.clone() : mainShape), 
				    (RectangleShape) ((titleShape != null) ? titleShape.clone() : titleShape), 
				    actionCompartmentShown, x, y, w, h,
				    comp != null);
  }

  protected void setShape(HorizontalCompartmentsShape mainShape,
			  RectangleShape titleShape, boolean showAction, 
			  float x, float y, float w, float h) { 
    super.setShape(mainShape, titleShape, ABOVE_TOP_LEFT, x, y, w, h);
    if (mainShape != null) {
      mainShape.setNumberOfHorizontalCompartmentsFixed(false);
      mainShape.setScaleTopCompartment(false);
      Compartments.DividerLineMode divMode;
      if (showAction) {
	divMode = Compartments.DividerLineMode.EveryLineExceptFirst;
      } else {
	divMode = Compartments.DividerLineMode.EveryLine;
      }
      mainShape.setDividerStyle(divMode, DrawAttr.DASHED_LINE);
      actionCompartmentShown = showAction;
    }
  }

  @Override
  public void copyTo(DrawShapeBase s) 
    throws CloneNotSupportedException {
    super.copyTo(s);
    if (s instanceof OrthogonalStateShape) {
      OrthogonalStateShape os = (OrthogonalStateShape) s;

    }
  }

  public int getNumberOfRegions() {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    int n = body.getNumberOfHorizontalCompartments();
    if (actionCompartmentShown) {
      return n - 1;
    }
    return n;
  }

  public Rectangle2D getRegionBounds(int i) {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    if (actionCompartmentShown) {
      return body.getHorizontalCompartmentBounds(i + 1); 
    } else { 
      return body.getHorizontalCompartmentBounds(i); 
    }
  }

  public Point2D getRegionCenter(int i) {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    if (actionCompartmentShown) {
      return body.getHorizontalCompartmentCenter(i + 1); 
    } else { 
      return body.getHorizontalCompartmentCenter(i); 
    }
  }  

  public float getRegionTop(int i) {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    float h0 = titleShape.getShapeHeight(); 
    if (actionCompartmentShown) {
      return body.getHorizontalCompartmentTop(i + 1) + h0; 
    } else { 
      return body.getHorizontalCompartmentTop(i) + h0; 
    }
  }

  public float getRegionHeight(int i) {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    if (actionCompartmentShown) {
      return body.getHorizontalCompartmentHeight(i + 1); 
    } else { 
      return body.getHorizontalCompartmentHeight(i); 
    }
  }

  public void setNumberOfRegions(int n) {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    int n0 = body.getNumberOfHorizontalCompartments();
    if (actionCompartmentShown) {
      n0 = n0 - 1;
    }
    if (n != n0) { 
      float[] div;
      if (actionCompartmentShown) {
	div = new float[n];
	float dh = 0.8f / n; 
	div[0] = 0.2f; 
	for (int i = 1; i < n; i++) { 
	  div[i] = dh; 
	}
      } else {
	div = new float[n - 1];
	float dh = 1f / n; 
	for (int i = 0; i < n - 1; i++) { 
	  div[i] = dh; 
	}
      }
      body.setHorizontalDividers(div);
    }
  }

  public void setRegionByHeight(float[] height) {
    if (height != null) { 
      int n = height.length; 
      if (n > 0) { 
	HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
	float h = 0;
	float h0 = 0; 
	if (actionCompartmentShown) {
	  h = h0 = body.getHorizontalCompartmentHeight(0); 
	}
	int i; 
	for (i = 0; i < n; i++) { 
	  h += height[i];
	}
	float[] div;
	if (actionCompartmentShown) {
	  div = new float[n];
	  div[0] = h0 / h; 
	  for (i = 0; i < n - 1; i++) { 
	    div[i + 1] = height[i] / h; 
	  }
	} else { 
	  div = new float[n - 1];
	  for (i = 0; i < n - 1; i++) { 
	    div[i] = height[i] / h; 
	  }
	}
	body.setHorizontalDividers(div);
      }
    }
  }

  public int inRegion(DrawShape s) {
    if (s != null) {
      HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
      int i = body.inHorizontalCompartment(s);
      if (i != -1) {
	if (actionCompartmentShown) {
	  if (i > 0) {
	    return (i - 1);
	  }
	}
	return i;
      }
    }
    return -1;
  }

  // redistribute the heights of the regions, total height remain the same, all regios are of the same height 
  public void evenRegionHeight() { 
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    float[] dividers = body.getHorizontalDividers();
    if (dividers != null) { 
      int n = dividers.length; 
      if (!actionCompartmentShown || n > 1) { 
	float div[] = new float[n]; 
	float h; 
	int start = 0; 
	if (actionCompartmentShown) {
	  h = (1f - dividers[0]) / n; 
	  start = 1; 
	  div[0] = dividers[0]; 
	} else { 
	  h = 1f / (n + 1); 
	}
	for (int i = start; i < n; i++) { 
	  div[i] = h; 
	}
	body.setHorizontalDividers(div);
      }
    }
  }

  // add a new region with the same height as the one under the mouse, shift all compartments below, expand size
  public void addRegion(int pos) { 
    int r = actionCompartmentShown ? pos - 2 : pos - 1;     
    //if (r < 0) r = 0; 

    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
    float[] dividers = body.getHorizontalDividers();
    float[] newDividers;
    float h1 = getRegionHeight(r);
    float h0 = body.getShapeHeight();
    if (dividers != null) { 
      List<GraphNodeBase>[] substates = findSubstatesInRegions();
      int n = dividers.length; 
      if (!actionCompartmentShown || n > 1) { 
	float div[] = new float[n + 1]; 
	float h = h0 + h1; 
	if (actionCompartmentShown) {
	  float haction = actionBox.getShapeHeight() + 2 * cpady;
	  div[0] = haction / h; 
	  for (int i = 1; i < n; i++) { 
	    if (i < r + 1) { 
	      div[i] = getRegionHeight(i) / h;
	    } if (i == r + 1) { 
	      div[i] = div[i + 1] = h1 / h;
	    } else { 
	      div[i + 1] = getRegionHeight(i) / h;
	    } 
	  }
	} else { 
	  for (int i = 0; i < n; i++) { 
	    if (i < r) { 
	      div[i] = getRegionHeight(i) / h;
	    } if (i == r) { 
	      div[i] = div[i + 1] = h1 / h;
	    } else { 
	      div[i + 1] = getRegionHeight(i) / h;
	    } 
	  }
	}
	body.setHorizontalDividers(div);

	for (int i = r + 1; i < substates.length; i++) { 
	  moveShapes(substates[i], 0, h1); 
	}
	moveEnd2(0, h1);
      }
    }
    //addHorizontalDivider(pos, 0);
  }

  // the region under the mouse is split into 2, the size of the whole state remain the same
  public void splitRegion(int pos) { 
    addHorizontalDivider(pos, 0);
  }

  public void positionSubNodes(boolean fit) {
    if (fit) { 
      float left = (float) Math.min(p1.getX(), p2.getX());
      float top = (float) Math.min(p1.getY(), p2.getY());
      float width = (float) Math.abs(p1.getX() - p2.getX());
      float height = (float) Math.abs(p1.getY() - p2.getY());
      
      Label header = getLabelShape(DrawShapeConstants.ROLE_NODE_TITLE_LABEL); 
      float w0 = 0, h0 = 0;      
      if (header != null) { 
	w0 = header.getShapeWidth() + 2 * cpady;
	h0 = header.getShapeHeight() + 2 * cpady;
      } else { 
	w0 = 2 * cpady;
	h0 = 2 * cpady;
      }

      float titleHeight = titleShape.getShapeHeight(); 
      float titleWidth = titleShape.getShapeWidth(); 
      if (w0 > titleWidth) { 
	titleShape.moveEdge(PointEastEdge, (w0 - titleWidth), 0);
	titleWidth = w0;
      }

      float actionHeight = 0;
      if (actionBox != null && actionBox.isVisible()) { 
	actionHeight = actionBox.getShapeHeight() + 2 * cpady;
      }

      // position substates 
      HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
      float bodyHeight = body.getShapeHeight();
      float[] dividers = body.getHorizontalDividers();
      int n = dividers.length;
      int nRegion = getNumberOfRegions(); 

      List<GraphNodeBase>[] regions = findSubstatesInRegions();
      float[] regionHeights = new float[regions.length];
      float regionTop  = top + titleHeight + actionHeight + DIVIDER_GAP; 
      float regionLeft = left + MARGIN_GAP;
      float dxall = 0, dyall = 0; 
      for (int i = 0; i < nRegion; i++) { 
	float rheight = getRegionHeight(i);
	Rectangle2D subbounds = getShapeBounds(regions[i]);
	float dx = 0, dy = 0; 
	float dx2 = 0, dy2 = 0;
	if (regions[i].size() > 0 && 
	    subbounds != null) { 	  
	  if (regionLeft > subbounds.getX()) { 
	    dx = regionLeft - (float) subbounds.getX();
	  }
	  if (regionTop > (float) subbounds.getY()) { 
	    dy = regionTop - (float) subbounds.getY();
	  }
	  if (dx > 0 || dy > 0) {
	    moveShapes(regions[i], dx, dy);
	  }
	  
	  if (debug) 
	    System.out.println("OrthogonalStateShape.positionSubNodes() [" + left + "," + top + "," + 
			       width + "," + height + "] dx=" + dx + " dy=" + dy + " subbouds=" + subbounds);
	  
	  float x2 = (float) subbounds.getX() + (float) subbounds.getWidth() + dx + MARGIN_GAP; 
	  float y2 = (float) subbounds.getY() + (float) subbounds.getHeight() + dy + DIVIDER_GAP; 
	  if (x2 > (left + width)) {
	    dx2 = (x2 - (left + width)); 
	    dxall = Math.max(dx2, dxall); 
	  }
	  if (y2 > (regionTop - DIVIDER_GAP + rheight)) { 
	    dy2 = (y2 - (regionTop - DIVIDER_GAP + rheight));
	    dyall += dy2;
	  }

	  for (int j = i + 1; j < nRegion; j++) { 
	     moveShapes(regions[j], 0, dy2);
	  }

	  if (debug) 
	    System.out.println("OrthogonalStateShape.positionSubNodes() x2=" + x2 + " y2=" + y2 +  
			       " dx=" + dx + " dy=" + dy);	  
	}
	regionHeights[i] = rheight + dy2; 
	regionTop += regionHeights[i];
      }

      if (dxall > 0 || dyall > 0) { 
	setEnds(left, top, left + width + dxall, top + height + dyall);
      }	

      float[] newDividers = new float[n];
      float stateHeight = height + dyall - titleHeight - actionHeight; 
      height = (actionHeight + stateHeight);
      int start = 0;
      if (actionHeight > 0) { 
	newDividers[0] = actionHeight / height;
	start = 1; 
      } 
      for (int i = start; i < n; i++) {
	newDividers[i] = regionHeights[i - start] / height;;
      }
      body.setHorizontalDividers(newDividers);      
    }
  }

  public List<GraphNodeBase>[] findSubstatesInRegions() { 
    int n = getNumberOfRegions(); 
    List<GraphNodeBase>[] result = new List[n];
    for (int i = 0; i < n; i++) { 
      result[i] = new ArrayList<GraphNodeBase>();
    }
    if (subshapes != null) {
      for (DrawShape s : subshapes) { 
	if (s.isVisible() && 
	    s instanceof GraphNodeBase) {
	  GraphNodeBase node = (GraphNodeBase) s;
	  int role = node.getRole();
	  if (role != DrawShapeConstants.ROLE_NODE_TITLE_LABEL &&
	      role != ACTION_ROLE && 
	      !node.isOnBoundary()) {
	    int r = inRegion(node); 
	    if (r >= 0 && r < n) { 
	      result[r].add(node);
	    }
	  }
	}
      }
    }
    return result; 
  }

  @Override
  public void showActionCompartment() {
    if (!actionCompartmentShown) {
      actionCompartmentShown = true;

      // necessary to sync with model, which may have been 
      // edited when the action box is hidden
      ModelElement model = getModel();
      if (model instanceof State) {
	State state = (State) model;
	setActions(state);
      }

      HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
      body.setDividerStyle(Compartments.DividerLineMode.EveryLineExceptFirst,
			   DrawAttr.DASHED_LINE);

      float[] dividers = body.getHorizontalDividers();
      float[] newDividers;
      float h;
      float h0 = body.getShapeHeight();
      if (actionBox != null) {
	actionBox.setVisible(true);
	float h1 = actionBox.getShapeHeight() + 2 * cpady;
	h = (h0 + h1);
	if (dividers != null && dividers.length > 0) {
	  int n = dividers.length;
	  newDividers = new float[n + 1];
	  newDividers[0] = h1 / h;
	  for (int i = 1; i <= n; i++) {
	    newDividers[i] = (dividers[i - 1] * h0) / h;
	  }
	} else {
	  newDividers = new float[] { h1 / h };
	}
	moveSubshapes(0, h1);
	moveEnd2(0, h1);
      } else {
	if (dividers != null && dividers.length > 0) {
	  int n = dividers.length;
	  newDividers = new float[n + 1];
	  newDividers[0] = 0.2f;
	  for (int i = 1; i <= n; i++) {
	    newDividers[i] = dividers[i - 1] * 0.8f;
	    ;
	  }
	} else {
	  newDividers = new float[] { 0.2f };
	}
	moveSubshapes(0, h0 * 0.25f);
	moveEnd2(0, h0 * 0.25f);
      }
      body.setHorizontalDividers(newDividers);

      positionActionBox();
      //fitCompartments();
      doLayout();
    }
  }

  @Override
  public void hideActionCompartment() {
    actionBox = findInRole(ACTION_ROLE);
    if (actionBox != null) {
      actionBox.setVisible(false);
    }

    if (mainShape instanceof HorizontalCompartmentsShape) { 
      HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;
      body.setDividerStyle(Compartments.DividerLineMode.EveryLine,
			   DrawAttr.DASHED_LINE);

      float h = body.getShapeHeight();
      float dy = 0;
      float[] dividers = body.getHorizontalDividers();
      float[] newDividers = null;
      if (dividers != null && dividers.length > 1) {
	dy = -h * dividers[0];
	int n = dividers.length;
	newDividers = new float[n - 1];
	float f = 1 / (1 - dividers[0]);
	for (int i = 1; i < n; i++) {
	  newDividers[i - 1] = dividers[i] * f;
	}
      }
      body.setHorizontalDividers(newDividers);
      moveSubshapes(0, dy);
      moveEnd2(0, dy);
      doLayout();
    }
    actionCompartmentShown = false;
  }

  // adjust dividers 
  public void fitCompartments(boolean keepWidth) {
    HorizontalCompartmentsShape body = (HorizontalCompartmentsShape) mainShape;

    float[] dividers = body.getHorizontalDividers();
    float[] newDividers;
    float hh = body.getShapeHeight();
    if (actionBox != null && 
	actionBox.isVisible()) { 
      float h0 = actionBox.getShapeHeight() + 2 * cpady;
      float h1 = body.getHorizontalCompartmentHeight(0); 
      float h2 = hh - h1; 
      float h = h0 + h2; 
      if (dividers != null && 
	  dividers.length > 0) {
	int n = dividers.length;
	newDividers = new float[n];
	newDividers[0] = h0 / h;
	for (int i = 1; i < n; i++) {
	  newDividers[i] = (dividers[i] * hh) / h;
	}
      } else {
	newDividers = new float[] { h0 / h };
      }
      float dy = h - hh; 
      moveSubshapes(0, dy);
      moveEnd2(0, dy);
      body.setHorizontalDividers(newDividers);
      positionActionBox();
    }
  }

  @Override
  public String toString() { 
    return "OrthogonalStateShape " + getStateName();
  }

  protected static final int DIVIDER_GAP   = 10;

}
